package com.lin.util;

import com.lin.model.Town;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.*;

/**
 * The {@code  TownConfigImpl} class implements the {@code  TownConfig} interface.
 * <p>
 * {@code  TownConfigImpl} is a map that stores towns using a {@code  Graph} underneath.
 * <p>
 * It wraps the functionality of {@code  Graph} and provides methods for accessing it using the town names.
 */
public class TownConfigImpl implements TownConfig {
    /**
     * {@code  Exception} that is thrown when a provided route is not valid because a town or road to
     * that town does not exist.
     */
    public static class NoSuchRouteException extends RuntimeException {
        public NoSuchRouteException(String message) {
            super(message);
        }
    }

    private Graph<Town> graph;
    private Map<String, Town> towns;

    /**
     * Creates a new {@code  TownConfigImpl}.
     */
    public TownConfigImpl() {
        graph = new Graph<>();
        towns = new HashMap<>();
    }

    /**
     * Add a town to this map.
     *
     * @param town {@code Town} to add to this map.
     */
    private void addTown(Town town) {
        graph.addNode(town);
        if (!towns.containsKey(town.getName())) {
            towns.put(town.getName(), town);
        }
    }


    @Override
    public void parseInput(String input) {
        if (input.length() < 2) {
            throw new IllegalArgumentException(PropertyFactory.getProperties().get("exception.town_map.illegal_argument.parse_input_format"));
        } else {
            String start = String.valueOf(input.charAt(0));
            String dest = String.valueOf(input.charAt(1));

            int weight = 0; // default weight is 0.

            if (input.length() > 2) {
                // If route weight is provided, try to parse it.
                try {
                    weight = Integer.parseInt(input.substring(2));
                } catch (NumberFormatException e) {
                    throw new IllegalArgumentException(PropertyFactory.propertyWithArgs("exception.town_map.illegal_argument.parse_input_weight",input));
                }
            }

            Town startTown = new Town(start);
            Town destTown = new Town(dest);

            addTown(startTown);
            addTown(destTown);
            graph.addEdge(startTown, destTown, weight);
        }
    }

    /**
     * AB5<br>
     * BC4<br>
     * CD8<br>
     * DC8<br>
     * DE6<br>
     * AD5<br>
     * CE2<br>
     * EB3<br>
     * AE7<br>
     *
     * @param path absolute path to the input file.
     * @throws FileNotFoundException if file not found at given path.
     */
    @Override
    public void init(String path) throws FileNotFoundException {
        Scanner in = new Scanner(new FileInputStream(path));
        while (in.hasNext()) {
            String input = in.next();
            parseInput(input);
        }
    }

    /**
     * {@code String} representing the shortest path between start and dest.
     *
     * @param start name of start town.
     * @param dest  name of town which we want to reach.
     * @return {@code String} representing the shortest path between start and dest.
     * @throws NoSuchRouteException if no route exists between the two towns.
     */
    @Override
    public String shortestPathBetween(String start, String dest) throws NoSuchRouteException {
        try {
            Graph.GraphPath path = graph.shortestPathBetween(towns.get(start), towns.get(dest));

            return path.toString();
        } catch (Graph.NodeNotReachableException | NoSuchElementException e) {
            throw new NoSuchRouteException(e.getMessage());
        }
    }

    /**
     * Distance of shortest path between start and dest.
     *
     * @param start name of start town.
     * @param dest  name of town which we want to reach.
     * @return length of the shortest path between start and dest.
     * @throws NoSuchRouteException if no route exists between the two towns.
     */
    @Override
    public int lengthOfShortestPathBetween(String start, String dest) throws NoSuchRouteException {
        try {
            Graph.GraphPath path = graph.shortestPathBetween(towns.get(start), towns.get(dest));

            return path.distance();
        } catch (Graph.NodeNotReachableException | NoSuchElementException e) {
            throw new NoSuchRouteException(e.getMessage());
        }
    }

    /**
     * Distance of route.
     * @param townNames town names in the order as they should be visited.
     * @return length of the route.
     * @throws NoSuchRouteException if no such route exists.
     */
    @Override
    public int distance(String... townNames) throws NoSuchRouteException {
        List<Town> townList = new ArrayList<Town>();
        for (String townName : townNames) {
            townList.add(towns.get(townName));
        }

        try {
            return graph.distance(townList);
        } catch (NoSuchElementException | Graph.NodeNotReachableException e) {
            throw new NoSuchRouteException(e.getMessage());
        }
    }

    /**
     * Count number of possible routes starting from start and ending at dest with a maximum number of {@code maxHops} .
     *
     * @param start   name of starting town of route.
     * @param dest    name of destination town of route.
     * @param maxHops maximum number of stops in route.
     * @return number of possible routes.
     */
    @Override
    public int countRoutesWithMaxHops(String start, String dest, int maxHops) {
        return graph.countRoutesWithMaxHops(towns.get(start), towns.get(dest), maxHops);
    }

    /**
     * Number of possible routes starting from start and ending at dest with a exactly {@code hops}  number of stops.
     *
     * @param start name of starting town of route.
     * @param dest  name of destination town of route.
     * @param hops  number of stops in route.
     * @return number of possible routes.
     */
    @Override
    public int countRoutesWithHops(String start, String dest, int hops) {
        return graph.countRoutesWithHops(towns.get(start), towns.get(dest), hops);
    }

    /**
     * Number of possible routes starting from start and ending at dest with a maximum distance of {@code maxDistance}.
     *
     * @param start       name of starting town of route.
     * @param dest        name of destination town of route.
     * @param maxDistance maximum allowed distance.
     * @return number of possible routes.
     */
    @Override
    public int countRoutesWithMaxDistance(String start, String dest, int maxDistance) {
        return graph.countRoutesWithMaxDistance(towns.get(start), towns.get(dest), maxDistance);
    }
}
